Skip to content

Conversation

@M4t7e
Copy link

@M4t7e M4t7e commented Oct 20, 2025

This PR introduces additional environment variables for load balancer configuration. These variables are designed to be set globally as defaults and can be overridden using annotations.

The main motivation is to improve support for GatewayAPI, as the Gateway annotation limit of 8 is restrictive and many settings are commonly needed across all load balancers from the same or even differen GatewayAPI providers. Additionally, this change allows environment-specific presets such as the new subnet IP range to be set globally. This removes the need to configure these settings in each service or use templating/patching to use the same service manifest for different environments.

New environment vars:

  • HCLOUD_LOAD_BALANCERS_ALGORITHM_TYPE
  • HCLOUD_LOAD_BALANCERS_DISABLE_PUBLIC_NETWORK
  • HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_INTERVAL
  • HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_RETRIES
  • HCLOUD_LOAD_BALANCERS_HEALTH_CHECK_TIMEOUT
  • HCLOUD_LOAD_BALANCERS_PRIVATE_SUBNET_IP_RANGE
  • HCLOUD_LOAD_BALANCERS_TYPE
  • HCLOUD_LOAD_BALANCERS_USES_PROXYPROTOCOL

@M4t7e M4t7e requested a review from a team as a code owner October 20, 2025 17:25
@apricote
Copy link
Member

Hey @M4t7e,

just to confirm, the "8 Annotations" limit is from the Gateway.spec.infrastructure.annotations field, right?

I found this thread where the limit was discussed and added, it sounds like this is a "soft" limit that can be raised if a reasonable case is made for more than 8 annotations: kubernetes-sigs/gateway-api#1757 (comment)

@M4t7e
Copy link
Author

M4t7e commented Oct 24, 2025

Hey @apricote,

yes, that's the limit I was referring to. I began researching ways to preset annotations in GatewayAPI, since the concept is not to have a single Gateway (Load Balancer) for everything, like it is often the case for Ingress Controller, but to have the flexibility of creating multiple Gateways. To avoid repeating the same config, I was looking into options setting global annotation setting. That's when I came across this issue: kubernetes-sigs/gateway-api#2734

From what I understand, the annotation limit can only be increased through provider-specific implementations like Istio and Envoy already support. In my case, I’m planning to use Cilium GatewayAPI, which as far as I know doesn’t support adding annotations with their custom config.

Btw, this is the actual issue where we want to add GatewayAPI support: hcloud-k8s/terraform-hcloud-kubernetes#216

@apricote
Copy link
Member

I personally dislike the many annotations, and have often wondered how we could provide a better alternative.

The Gateway API parameterRef looks better, but a Gateway API Provider needs to implement L7 functionality, which our Load Balancer currently do not provide. So I figured that it makes no sense to built our own Gateway API Provider.


@lukasmetzner will be back next week to take a closer look at the MR.

@codecov
Copy link

codecov bot commented Oct 28, 2025

Codecov Report

❌ Patch coverage is 73.17073% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.76%. Comparing base (1aa4980) to head (33b6571).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/config/config.go 60.41% 8 Missing and 11 partials ⚠️
internal/hcops/load_balancer.go 81.33% 10 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1052      +/-   ##
==========================================
- Coverage   68.29%   64.76%   -3.54%     
==========================================
  Files          23       23              
  Lines        2520     2608      +88     
==========================================
- Hits         1721     1689      -32     
- Misses        629      745     +116     
- Partials      170      174       +4     
Flag Coverage Δ
e2e ?
unit 64.76% <73.17%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lukasmetzner
Copy link
Contributor

e2e test not passing is fine here. This is an expected permission issue.

}

if algorithmType, ok := os.LookupEnv(hcloudLoadBalancersAlgorithmType); ok {
alg, parseErr := parseLoadBalancerAlgorithmType(algorithmType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are mixing Read and Validate in this call. Could you please move the arg type validation into Validate.

Comment on lines +220 to +222
if subnetRange, ok := os.LookupEnv(hcloudLoadBalancersPrivateSubnetIPRange); ok {
cfg.LoadBalancer.PrivateSubnetIPRange = subnetRange
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use net.ParseCIDR in the Validate function to check if the format is correct.

Comment on lines +233 to +240
if interval, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckInterval); ok {
d, parseErr := time.ParseDuration(interval)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckInterval, parseErr))
} else {
cfg.LoadBalancer.HealthCheckInterval = d
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if interval, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckInterval); ok {
d, parseErr := time.ParseDuration(interval)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckInterval, parseErr))
} else {
cfg.LoadBalancer.HealthCheckInterval = d
}
}
cfg.LoadBalancer.HealthCheckInterval, err = getEnvDuration(hcloudLoadBalancersHealthCheckInterval)
if err != nil {
errs = append(errs, err)
}

We already have a util function for this. If errs is not empty, HCCM won't continue and throw an error. So the if-else is not necessary here.

Comment on lines +242 to +249
if timeout, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckTimeout); ok {
d, parseErr := time.ParseDuration(timeout)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckTimeout, parseErr))
} else {
cfg.LoadBalancer.HealthCheckTimeout = d
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if timeout, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckTimeout); ok {
d, parseErr := time.ParseDuration(timeout)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckTimeout, parseErr))
} else {
cfg.LoadBalancer.HealthCheckTimeout = d
}
}
cfg.LoadBalancer.HealthCheckTimeout, err = getEnvDuration(hcloudLoadBalancersHealthCheckTimeout)
if err != nil {
errs = append(errs, err)
}

Same here

Comment on lines +251 to +258
if retries, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckRetries); ok {
v, parseErr := strconv.Atoi(retries)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckRetries, parseErr))
} else {
cfg.LoadBalancer.HealthCheckRetries = v
}
}
Copy link
Contributor

@lukasmetzner lukasmetzner Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if retries, ok := os.LookupEnv(hcloudLoadBalancersHealthCheckRetries); ok {
v, parseErr := strconv.Atoi(retries)
if parseErr != nil {
errs = append(errs, fmt.Errorf("failed to parse %s: %w", hcloudLoadBalancersHealthCheckRetries, parseErr))
} else {
cfg.LoadBalancer.HealthCheckRetries = v
}
}
cfg.LoadBalancer.HealthCheckRetries, err = strconv.Atoi(os.Getenv(hcloudLoadBalancersHealthCheckRetries))
if err != nil {
errs = append(errs, err)
}

Same here

Comment on lines +265 to +267
if lbType, ok := os.LookupEnv(hcloudLoadBalancersType); ok {
cfg.LoadBalancer.Type = lbType
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if lbType, ok := os.LookupEnv(hcloudLoadBalancersType); ok {
cfg.LoadBalancer.Type = lbType
}
cfg.LoadBalancer.Type = os.Getenv(hcloudLoadBalancersType)

cfg.LoadBalancer.Type is of type string and therefore will be initialized as an empty string. os.Getenv will return an empty string when the env is not set.

Comment on lines +1090 to +1093
// Workaround to keep bug https://github.com/hetznercloud/hcloud-cloud-controller-manager/issues/876
if b.cfg.ProxyProtocolEnabled != nil {
b.proxyProtocol = hcloud.Ptr(*b.cfg.ProxyProtocolEnabled)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Workaround to keep bug https://github.com/hetznercloud/hcloud-cloud-controller-manager/issues/876
if b.cfg.ProxyProtocolEnabled != nil {
b.proxyProtocol = hcloud.Ptr(*b.cfg.ProxyProtocolEnabled)
}
b.proxyProtocol = b.cfg.ProxyProtocolEnabled

The comment is not entirely true here. The bug is about resetting the value of b.proxyProtocol when neither the annotation nor a global default is set. Essentially setting it back to its API default when the annotation is removed. Also, the comment does not describe the problem properly. The issue is, that this would be a breaking change, as users might have removed the annotation but still rely on the proxy protocol being enabled. This would silently change that.

We can also simply the statement a bit. hcloud.Ptr is just returning a pointer to the argument passed in (e.g., hcloud.Ptr(true) is a common use-case). So dereferencing the pointer and creating another reference does not add any value. Furthermore, b.proxyProtocol is initialized with nil, so if we reach this branch of the if statement we might as well just use b.cfg.ProxyProtocolEnabled directly.

Comment on lines +1265 to +1270
b.healthCheckOpts.Interval = hcloud.Ptr(hcInterval)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckInterval != 0 {
b.healthCheckOpts.Interval = hcloud.Ptr(b.cfg.HealthCheckInterval)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
b.healthCheckOpts.Interval = hcloud.Ptr(hcInterval)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckInterval != 0 {
b.healthCheckOpts.Interval = hcloud.Ptr(b.cfg.HealthCheckInterval)
b.healthCheckOpts.Interval = &hcInterval
b.addHealthCheck = true
return nil
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckInterval != 0 {
b.healthCheckOpts.Interval = &b.cfg.HealthCheckInterval

No need to use hcloud.Ptr here. I don't know why it was used before though.

Comment on lines +1282 to +1287
b.healthCheckOpts.Timeout = hcloud.Ptr(t)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckTimeout != 0 {
b.healthCheckOpts.Timeout = hcloud.Ptr(b.cfg.HealthCheckTimeout)
Copy link
Contributor

@lukasmetzner lukasmetzner Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
b.healthCheckOpts.Timeout = hcloud.Ptr(t)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckTimeout != 0 {
b.healthCheckOpts.Timeout = hcloud.Ptr(b.cfg.HealthCheckTimeout)
b.healthCheckOpts.Timeout = &t
b.addHealthCheck = true
return nil
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckTimeout != 0 {
b.healthCheckOpts.Timeout = &b.cfg.HealthCheckTimeout

Similar we don't need to use hcloud.Ptr here.

Comment on lines +1299 to +1304
b.healthCheckOpts.Retries = hcloud.Ptr(v)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckRetries != 0 {
b.healthCheckOpts.Retries = hcloud.Ptr(b.cfg.HealthCheckRetries)
Copy link
Contributor

@lukasmetzner lukasmetzner Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
b.healthCheckOpts.Retries = hcloud.Ptr(v)
b.addHealthCheck = true
return nil
}
if err != nil {
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckRetries != 0 {
b.healthCheckOpts.Retries = hcloud.Ptr(b.cfg.HealthCheckRetries)
b.healthCheckOpts.Retries = &v
b.addHealthCheck = true
return nil
} else if errors.Is(err, annotation.ErrNotSet) {
if b.cfg.HealthCheckRetries != 0 {
b.healthCheckOpts.Retries = &b.cfg.HealthCheckRetries

Similar we don't need to use hcloud.Ptr here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants